"""
Copyright 2009, Thomas Dejanovic, Jay Shurtz.
 
This is free software; you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as
published by the Free Software Foundation; either version 2.1 of
the License, or (at your option) any later version.

This software is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this software; if not, write to the Free
Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
02110-1301 USA, or see the FSF site: http://www.fsf.org.

User interface to the physical implementation of a hatched design, 
such as an FPGA using a serial port.

This is the module that should be used to debug and communicate with
your designs (using register reads & writes.)  It is essentially a
useful wrapper around 'hatch_facade.py' that allows everything to be
configured directly from the command line.

Run this file from the command line with the '-h' option to see example usages.
"""

HEAD_URL = "$HeadURL: http://hatch.googlecode.com/svn/tags/taggle_release_2.2/hatch_interface/hatch_interface.py $"
REVISION = "$Revision: 736 $"
AUTHOR = "$Author: jayshurtz $"
DATE = "$Date: 2010-10-08 06:18:29 +0000 (Fri, 08 Oct 2010) $"
ID = "$Id: hatch_interface.py 736 2010-10-08 06:18:29Z jayshurtz $"
version = " ".join(ID.split()[2:4])


import optparse


import to_hatch_structure
del to_hatch_structure
import serial
import hatch_console    # History, tab-complete, logging, etc.
import hatch_facade
import hatch_serial
# XXX import other port wrappers (like serial) here.


from os import path as ospath   
fileName = ospath.splitext(ospath.basename(__file__))[0] # Get the file name without the extension.


# Define hatch bus types.
dummy = {'portType': 'dummy'}

dcom_apb = {'portType': 'serial', 'opcodes': repr({'read':0x21,'write':0x22}),
    'byteOrder': '>', 'format': 'B', 'opCodeLen': 1, 'addressLen': 4,
    'dataLen': 4, 'baudrate': 115200, 'bytesize': serial.EIGHTBITS,
    'parity': serial.PARITY_NONE, 'stopbits': serial.STOPBITS_ONE,
    'timeout': hatch_serial.DEFAULT_SERIAL_TIMEOUT, 'xonxoff': 0, 'rtscts': 0,
    'writeTimeout': None, 'dsrdtr': 0, 'interCharTimeout': None}

byron = {'portType': 'serial', 'opcodes': repr({'read':0x25,'write':0x24}),
    'byteOrder': '>', 'format': 'B', 'opCodeLen': 1, 'addressLen': 1,
    'dataLen': 1, 'baudrate': 19200, 'bytesize': serial.EIGHTBITS, 
    'parity': serial.PARITY_NONE, 'stopbits': serial.STOPBITS_ONE, 
    'timeout': hatch_serial.DEFAULT_SERIAL_TIMEOUT, 'xonxoff': 0, 'rtscts': 0, 
    'writeTimeout': None, 'dsrdtr': 0, 'interCharTimeout': None}

# XXX Define new interfaces here as dictionaries.


class HatchOptions(hatch_console.ClassOptions):
    """ Make "using command line arguments as parameters in the creation of a 
        HatchFacade" easy to implement.
          this -> default options dict
          this -> optparse option groups
          this -> HatchFacade instance
    """
    optionDefaults = {}

    # Option defaults for hatch.
    optionDefaults["xmlFile"] = 'None'
    optionDefaults["delimiter"] = '.'
    optionDefaults["bus"] = 'dcom_apb'
    optionDefaults["opcodes"] = "{'read':0x21,'write':0x22}"
    optionDefaults["opCodeLen"] = 1
    optionDefaults["addressLen"] = 1
    optionDefaults["dataLen"] = 1
    optionDefaults["byteOrder"] = '>'
    optionDefaults["format"] = 'B'
    optionDefaults["portType"] = 'serial'

    # Option defaults for hatch serial port.
    optionDefaults["port"] = hatch_serial.DEFAULT_SERIAL_PORT 
    optionDefaults["baudrate"] = hatch_serial.DEFAULT_SERIAL_BAUDRATE
    optionDefaults["bytesize"] = hatch_serial.serial.EIGHTBITS
    optionDefaults["parity"] = hatch_serial.serial.PARITY_NONE
    optionDefaults["stopbits"] = hatch_serial.serial.STOPBITS_ONE
    optionDefaults["timeout"] = hatch_serial.DEFAULT_SERIAL_TIMEOUT
    optionDefaults["xonxoff"] = 0
    optionDefaults["rtscts"] = 0
    optionDefaults["writeTimeout"] = None
    optionDefaults["dsrdtr"] = 0

    def __init__(self, name='', shortOptions={}, options={}, delimiter="_"):
        """ Example: 
                hi = HatchOptions('hatch', 
                    {'xmlFile': '-x', 'bus': '-b', 'port': '-p'}, 
                    {'port': '/dev/ttyUSB0'})
                aa = HatchOptions('attenuator', 
                    {'xmlFile': '-y', 'port': '-a'}, 
                    {'port': '/dev/ttyUSB1'})
        """
        self.name = name
        self.shortOptions = shortOptions
        self.delimiter = delimiter
        _options = self.optionDefaults.copy()
        _options.update(options)
        self.options = self._busOptions(_options)   # Replace with bus values.

    def _busOptions(self, _dict):
        """ Replace values in _dict with values from the bus type 
            dictionary, which itself is a value in _dict.
        """
        bus = {}    # Empty bus.
        try:
            bus = eval(str(_dict.get('bus', '{}')))    # Evaluate dictionary.
        except (NameError, TypeError), e:
            pass    # Ignore if no bus or eval() fails.
        _dict.update(bus)
        return _dict

    def optionsToDict(self):
        """ Return a dictionary of options with unique keys.
        """
        self.options = self._busOptions(self.options)   # Replace bus values.
        return hatch_console.ClassOptions.optionsToDict(self)

    def dictToOptions(self, _dict):
        """ Convert dictionary of unique options to generic parameters for this instance.
        """
        hatch_console.ClassOptions.dictToOptions(self, _dict)
        self.options = self._busOptions(self.options) # Replace bus values.

    def addOptionGroups(self, parser, defaultString="Default: "):
        """ Add options to parser.
        """
        #self.options = self._busOptions(self.options)  # Replace bus values.
        self.addHatchFacadeGroup(parser, defaultString)
        self.addSerialPortGroup(parser, defaultString)

    def create(self, options, logHandlers=[]):
        """ Return a HatchFacade instance.
            'options' should be a dictionary returned by optparse, ie:
              >>> (options, args) = parser.parse_args()
              >>> options = options.__dict__
        """
        self.dictToOptions(options) # Take unique name out of keys.
        return self.getHatchFacade(logHandlers, portWrapper=None)

    # Option parser functions.
    def addHatchFacadeGroup(self, parser, defaultString="Default: "):
        """ Options commented out are for experts only.
        """
        options = self.options
        shortOptions = self.shortOptions
        facadeGroup = optparse.OptionGroup(parser, (self.name.title() + " Hatch Facade Options").strip(), "")
        facadeGroup.description = "The opcode, address, and data length values are in units specified"
        facadeGroup.description += " by the format value. See hatch_serial.py for details."
        xml = optparse.Option(shortOptions.get('xmlFile', None), self.getOpt('xml'), dest=self.addName('xmlFile'), metavar="FILE")
        xml.help = "Hatched XML design file. Use 'None' for no " + (self.name + " facade.").strip()
        xml.help += " " + defaultString + repr(options["xmlFile"])
        facadeGroup.add_option(xml)
        #delimiter = optparse.Option(self.getOpt('delimiter'), dest=self.addName('delimiter'), metavar="CHR")
        #delimiter.type, delimiter.choices = "choice", ['_','__','.']
        #delimiter.help = "Specify a flat '_' or nested '.' facade by delimiter. " + defaultString + repr(options["delimiter"])
        #facadeGroup.add_option(delimiter)
        busChoices = ['None', 'dummy', 'dcom_apb', 'byron']
        bus = optparse.Option(shortOptions.get('bus', None), self.getOpt('bus'), dest=self.addName('bus'), metavar="BUS")
        bus.type, bus.choices = "choice", busChoices
        bus.help = "Specify a pre-defined bus to automatically set port options, opcodes, address length, etc."
        bus.help += " Options are: " + repr(busChoices) + ". " + defaultString + repr(options["bus"])
        facadeGroup.add_option(bus)
        #opcodes = optparse.Option(self.getOpt('opcodes'), dest=self.addName('opcodes'), metavar="STR")
        #opcodes.help = "Must be a string that evaluates to a python dictionary. "
        #opcodes.help += defaultString + repr(options["opcodes"])
        #facadeGroup.add_option(opcodes)
        #opCodeLen = optparse.Option(self.getOpt('opcode-length'), dest=self.addName('opCodeLen'), type="int", metavar="INT")
        #opCodeLen.help = defaultString + repr(options["opCodeLen"])
        #facadeGroup.add_option(opCodeLen)
        #addressLen = optparse.Option(self.getOpt('address-length'), dest=self.addName('addressLen'), type="int", metavar="INT",)
        #addressLen.help = defaultString + repr(options["addressLen"])
        #facadeGroup.add_option(addressLen)
        #dataLen = optparse.Option(self.getOpt('data-length'), dest=self.addName('dataLen'), type="int", metavar="INT",)
        #dataLen.help = defaultString + repr(options["dataLen"])
        #facadeGroup.add_option(dataLen)
        #byteOrder = optparse.Option(self.getOpt('byte-order'), dest=self.addName('byteOrder'), metavar="CHR")
        #byteOrder.help = defaultString + repr(options["byteOrder"]) + " (See python struct module.)"
        #facadeGroup.add_option(byteOrder)
        #format = optparse.Option(self.getOpt('format'), dest=self.addName('format'), metavar="CHR")
        #format.help = defaultString + repr(options["format"]) + " (See python struct module.)"
        #facadeGroup.add_option(format)
        #portTypeChoices = ['dummy', 'serial']    # XXX 'usb', etc...
        #portType = optparse.Option(self.getOpt('port-type'), dest=self.addName('portType'), metavar="STR")
        #portType.type, portType.choices = "choice", portTypeChoices
        #portType.help = "Type of device port to use."
        #portType.help += " Options are: " + repr(portTypeChoices) + ". " + defaultString + repr(options["portType"])
        #facadeGroup.add_option(portType)
        parser.add_option_group(facadeGroup) 

    def addSerialPortGroup(self, parser, defaultString="Default: "):
        """ Options commented out are unsupported by the current hardware.
        """
        options = self.options
        shortOptions = self.shortOptions
        serialPortGroup = optparse.OptionGroup(parser, (self.name.title() + " Serial Port Options").strip(), "See pyserial for details.")
        port = optparse.Option(shortOptions.get('port', None), self.getOpt('port'), dest=self.addName('port'), metavar="STR")
        port.help = "Device port. Use 'None' for no " + (self.name + " facade.").strip()
        port.help += " " + defaultString + repr(options["port"])
        serialPortGroup.add_option(port)
        baudrate = optparse.Option(self.getOpt('baudrate'), type="int", dest=self.addName('baudrate'), metavar="INT") 
        baudrate.help = defaultString + repr(options["baudrate"])
        serialPortGroup.add_option(baudrate)
        #bytesize = optparse.Option(self.getOpt('bytesize'), type="int", dest=self.addName('bytesize'), metavar="INT")
        #bytesize.help = defaultString + repr(options["bytesize"])
        #serialPortGroup.add_option(bytesize)
        #parity = optparse.Option(self.getOpt('parity'), dest=self.addName('parity'), metavar="CHR")
        #parity.help = defaultString + repr(options["parity"])
        #serialPortGroup.add_option(parity)
        #stopbits = optparse.Option(self.getOpt('stopbits'), type="int", dest=self.addName('stopbits'), metavar="INT")
        #stopbits.help = defaultString + repr(options["stopbits"])
        #serialPortGroup.add_option(stopbits)
        timeout = optparse.Option(self.getOpt('timeout'), type="float", dest=self.addName('timeout'), metavar="FLT")
        timeout.help = defaultString + repr(options["timeout"])
        serialPortGroup.add_option(timeout)
        #xonxoff = optparse.Option(self.getOpt('xonxoff'), type="int", dest=self.addName('xonxoff'), metavar="INT")
        #xonxoff.help = defaultString + repr(options["xonxoff"])
        #serialPortGroup.add_option(xonxoff)
        #rtscts = optparse.Option(self.getOpt('rtscts'), type="int", dest=self.addName('rtscts'), metavar="INT")
        #rtscts.help = defaultString + repr(options["rtscts"])
        #serialPortGroup.add_option(rtscts)
        #writeTimeout = optparse.Option(self.getOpt('write-timeout'), type="int", dest=self.addName('write-timeout'), metavar="INT")
        #writeTimeout.help = defaultString + repr(options["writeTimeout"])
        #serialPortGroup.add_option(writeTimeout)
        #dsrdtr = optparse.Option(self.getOpt('dsrdtr'), type="int", dest=self.addName('dsrdtr'), metavar="INT")
        #dsrdtr.help = defaultString + repr(options["dsrdtr"])
        #serialPortGroup.add_option(dsrdtr)
        ##interCharTimeout = optparse.Option(self.getOpt('interchartimeout'), type="int", dest=self.addName('interchartimeout', delimiter), metavar="INT")
        ##interCharTimeout.help = defaultString + repr(options["interCharTimeout"])
        ##serialPortGroup.add_option(interCharTimeout)
        parser.add_option_group(serialPortGroup) 

    # Hatch facade setup functions.
    def getHatchPortWrapper(self, logHandlers=[]):
        """ """
        options = self.options
        portWrapper = None
        if options['portType'] == 'serial':
            try:    # Serial ports call self.open() inside __init__(), bad if port is already open.
                port = serial.Serial(port=options['port'], \
                    baudrate=options['baudrate'], bytesize=options['bytesize'], \
                    parity=options['parity'], stopbits=options['stopbits'], \
                    timeout=options['timeout'], xonxoff=options['xonxoff'], \
                    rtscts=options['rtscts'], writeTimeout=options['writeTimeout'], \
                    dsrdtr=options['dsrdtr']) #,interCharTimeout=options['interCharTimeout'])
            except serial.SerialException, e: # Raise a more generic exception. 
                raise IOError, (37, str(e))
            portWrapper = hatch_serial.HatchPortWrapperSerial(port, logHandlers)
        # XXX specify further options here.
        else:   # Use dummy interface by default.
            portWrapper = hatch_target.HatchPortWrapperDummy(logHandlers=logHandlers)
        return portWrapper

    def getHatchFacade(self, logHandlers=[], portWrapper=None):
        """ Setup a hatch facade based on the dictionary of command line options.
            Can be called multiple times for multiple pieces of hardware.
        """
        options = self.options
        hatchFacade = None
        xmlFile = options.get('xmlFile', 'None')
        if xmlFile != 'None' and options.get("port", 'None') != 'None':      
            if portWrapper is None:
                portWrapper = self.getHatchPortWrapper(logHandlers)
            #if xmlFile is 'None':
            #    raise AssertionError, (self.name.title() + " XML file option not specified").strip()
            hatchFacadeFactory = hatch_facade.HatchFacadeFactory(None, logHandlers)
            #if options['delimiter'] == '.':
            hatchFacade = hatch_facade.HatchFacadeNested(portWrapper, xmlFile, hatchFacadeFactory, \
                eval(options['opcodes']), options['byteOrder'], options['format'], \
                options['opCodeLen'], options['addressLen'], options['dataLen'])
            #else:
            #    hatchFacade = hatch_facade.HatchFacadeBasic(portWrapper, xmlFile, hatchFacadeFactory, options['delimiter'])
        return hatchFacade


# Option parser functions.
def getOptionParser(optionDefaults, optsList=[], defaultString="Default: "):
    """ """
    parser = optparse.OptionParser(epilog="\n\n", version="%prog " + str(version))
    parser.usage = "%prog [options]"
    parser.usage += "\n\n  Example usage:"
    parser.usage += "\n    python -i %prog -x hatched.xml -b dcom_apb -p COM1 -l debug"
    parser.usage += "\n\n  To run without a hatch facade, specify -x None or -p None:"
    parser.usage += "\n    python -i %prog -p None"
    parser.description = "User interface to a hatched hardware design."
    parser.formatter = optparse.TitledHelpFormatter() #IndentedHelpFormatter()
    hatch_console.addScriptGroup(parser, optionDefaults, defaultString)
    for hatchOpts in optsList:
        hatchOpts.addOptionGroups(parser)  # Add hatchFacade options.
    return parser


# Easy setup functions for scripts that import this file.
def setupHatchFacade(hatchOpts, options, logger, logHandlers=[]):
    """ """
    hatchFacade = hatchOpts.create(options, logHandlers)
    if hatchFacade is not None:
        logger.info("Set up " + hatchOpts.name + " facade.")
    else:
        logger.warning("No " + hatchOpts.name + " design file or port specified.")
    return hatchFacade


# Easy setup functions that do not use a command line options dictionary.
# TODO - add 'baudrate' & 'timeout' function args ?
def getHatchFacade(port, bus, xmlFile, logHandlers=[]):
    """ Get a hatch facade specifying only the port, bus, and XML file.
        In the future, bus parameters may be specified by the XML file.
    """
    options = {}
    options['port'] = port
    options['bus'] = bus
    options['xmlFile'] = xmlFile
    hatchOpts = HatchOptions(options=options)
    hatchFacade = hatchOpts.create({}, logHandlers)
    return hatchFacade


def getSerialLPCB(port, xmlFile, logHandlers=[]):
    """ Get a hatch facade that uses the serial LPCB/dcom APB bus.
        For example:
          >>> hi = getSerialLPCB('/dev/ttyUSB0', 'base_station_hatch.xml')
    """
    return getHatchFacade(port, 'dcom_apb', xmlFile, logHandlers)


def getSerialByron(port, xmlFile, logHandlers=[]):
    """ Get a hatch facade that uses the serial Byron bus.
        For example:
          >>> hi = getSerialLPCB('/dev/ttyUSB0', 'byron_hatch.xml')
    """
    return getHatchFacade(port, 'byron', xmlFile, logHandlers)


# Main function.
if __name__ == "__main__":

    import logging
    import traceback

    try:
        # Set-up a logger for this script, default to debug logging.
        logger = logging.getLogger(fileName)
        logger.setLevel(logging.DEBUG)

        # Define option defaults.
        optionDefaults = hatch_console.optionDefaults.copy()

        # Option defaults for hatch.
        hatchOpts = HatchOptions('hatch', {'xmlFile': '-x', 'bus': '-b', 
            'port': '-p'}, {'bus': 'byron'})
        optionDefaults.update(hatchOpts.optionsToDict())

        # Get option parser.
        parser = getOptionParser(optionDefaults, [hatchOpts])

        # Get command line options and defaults.
        options = hatch_console.getCommandLineOptions(parser)

        # XXX Note that if the user has set 'timeout' and 'bus', the bus
        # timeout will be used, as explicitly set options are not stored 
        # seperately from default options. This problem will go away when 
        # the bus values are specified by the XML file. For now, code should
        # default to the correct bus type for a given design.
        options = hatch_console.setDefaults(options, optionDefaults)

        # Setup console.
        if True:
            logHandlers = hatch_console.setupConsole(options, logger, fileName)
        else:   # Setup logging with a special log formatter.
            logHandlers = hatch_console.setupConsole(options, logger, fileName, 
                hatch_console.baseStationLogFormatter(options['logFileFormat'], 
                options['logFileFormatTime']))

        # Setup interface to hardware.
        hi = setupHatchFacade(hatchOpts, options, logger, logHandlers)

    except SystemExit, e:
        pass
    except Exception, e:    # Log and raise all errors.
        logger.error(traceback.format_exc())
        raise
    else:
        pass #logger.info("Done.")


